/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/*
 * ZorLogFrame.java
 *
 * Created on 21.06.2011, 13:46:10
 */
package com.transas.zorlog.ui;

import com.transas.zorlog.data.DataSource;
import com.transas.zorlog.data.NullDataSource;
import com.transas.zorlog.data.log.LogLoaderWorker;
import com.transas.zorlog.data.log.LogSaverWorker;
import com.transas.zorlog.flightgear.FlightGearSender;
import com.transas.zorlog.gcslink.COMStream;
import com.transas.zorlog.gcslink.GCSLink;
import com.transas.zorlog.gcslink.SerialStream;
import com.transas.zorlog.gcslink.UDPStream;
import com.transas.zorlog.ui.graph.Graph;
import com.transas.zorlog.ui.graph.GraphPane;
import com.transas.zorlog.ui.graph.GraphPaneConfig;
import com.transas.zorlog.worldwind.WorldWindFrame;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;



/**
 *
 * @author amalikov
 */
public class ZorLogFrame extends javax.swing.JFrame {

	private static final String APPTITLE = "ZorLog v1.5";	
	
	/**
	 * @param args the command line arguments
	 */
	public static void main(String args[]) {
		java.awt.EventQueue.invokeLater(new Runnable() {
			@Override
			public void run() {
				new ZorLogFrame().setVisible(true);
			}
		});
	}
	
	/** 
	 * Creates new form ZorLogFrame
	 */
	public ZorLogFrame() {
		initComponents();
		initGlassPane();
		panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
		setLocationRelativeTo(null);	// center on screen
		updateTitle(null);

		appConfig = new AppConfig(getHomeDir() + "/data/config/zorlog.xml");
		appConfig.read();
		
		formatList = new FormatList(getHomeDir() + "/data/formats");
		formatList.read();
		
		applyPosSize();
		
		panel.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0), "doZoomIn");
		panel.getActionMap().put("doZoomIn", new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent e) {
				Point p = panel.getMousePosition();
				panelMouseWheelMoved(new MouseWheelEvent(panel, 0, 0, 0, p.x, p.y, 0, false, 0, 0, -1));
			}
		});

		panel.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "doZoomOut");
		panel.getActionMap().put("doZoomOut", new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent e) {
				Point p = panel.getMousePosition();
				panelMouseWheelMoved(new MouseWheelEvent(panel, 0, 0, 0, p.x, p.y, 0, false, 0, 0, 1));
			}
		});
	}

	/** This method is called from within the constructor to
	 * initialize the form.
	 * WARNING: Do NOT modify this code. The content of this method is
	 * always regenerated by the Form Editor.
	 */
	@SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        panel = new javax.swing.JPanel();
        javax.swing.JMenuBar menu = new javax.swing.JMenuBar();
        javax.swing.JMenu menuFile = new javax.swing.JMenu();
        javax.swing.JMenuItem menuFileOpen = new javax.swing.JMenuItem();
        menuFileSave = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator1 = new javax.swing.JPopupMenu.Separator();
        javax.swing.JMenuItem menuFileOpenPreset = new javax.swing.JMenuItem();
        javax.swing.JMenuItem menuFileSavePreset = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator2 = new javax.swing.JPopupMenu.Separator();
        javax.swing.JMenuItem menuFileExit = new javax.swing.JMenuItem();
        javax.swing.JMenu menuMap = new javax.swing.JMenu();
        javax.swing.JMenuItem menuMapShow = new javax.swing.JMenuItem();
        menuMapOptions = new javax.swing.JMenuItem();
        menuFG = new javax.swing.JMenu();
        menuFGEnable = new javax.swing.JCheckBoxMenuItem();
        menuFGOptions = new javax.swing.JMenuItem();
        menuGCS = new javax.swing.JMenu();
        menuGCSEnable = new javax.swing.JCheckBoxMenuItem();
        menuGCSOptions = new javax.swing.JMenuItem();
        javax.swing.JMenu menuPlayback = new javax.swing.JMenu();
        javax.swing.JMenuItem menuPlaybackPlay = new javax.swing.JMenuItem();
        javax.swing.JMenuItem menuPlaybackPause = new javax.swing.JMenuItem();
        javax.swing.JMenuItem menuPlaybackStop = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
        setTitle("ZorLog");
        setMinimumSize(new java.awt.Dimension(200, 100));
        setName("frame"); // NOI18N
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosed(java.awt.event.WindowEvent evt) {
                formWindowClosed(evt);
            }
        });

        panel.setOpaque(false);
        panel.addMouseWheelListener(new java.awt.event.MouseWheelListener() {
            public void mouseWheelMoved(java.awt.event.MouseWheelEvent evt) {
                panelMouseWheelMoved(evt);
            }
        });
        panel.addMouseListener(new java.awt.event.MouseAdapter() {
            public void mousePressed(java.awt.event.MouseEvent evt) {
                panelMousePressed(evt);
            }
            public void mouseReleased(java.awt.event.MouseEvent evt) {
                panelMouseReleased(evt);
            }
        });
        panel.addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
            public void mouseDragged(java.awt.event.MouseEvent evt) {
                panelMouseDragged(evt);
            }
            public void mouseMoved(java.awt.event.MouseEvent evt) {
                panelMouseMoved(evt);
            }
        });

        javax.swing.GroupLayout panelLayout = new javax.swing.GroupLayout(panel);
        panel.setLayout(panelLayout);
        panelLayout.setHorizontalGroup(
            panelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 800, Short.MAX_VALUE)
        );
        panelLayout.setVerticalGroup(
            panelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 579, Short.MAX_VALUE)
        );

        menuFile.setText("File");

        menuFileOpen.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_O, java.awt.event.InputEvent.CTRL_MASK));
        menuFileOpen.setText("Open");
        menuFileOpen.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuFileOpenActionPerformed(evt);
            }
        });
        menuFile.add(menuFileOpen);

        menuFileSave.setText("Save");
        menuFileSave.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuFileSaveActionPerformed(evt);
            }
        });
        menuFile.add(menuFileSave);
        menuFile.add(jSeparator1);

        menuFileOpenPreset.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_O, java.awt.event.InputEvent.ALT_MASK));
        menuFileOpenPreset.setText("Open preset");
        menuFileOpenPreset.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuFileOpenPresetActionPerformed(evt);
            }
        });
        menuFile.add(menuFileOpenPreset);

        menuFileSavePreset.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_S, java.awt.event.InputEvent.ALT_MASK));
        menuFileSavePreset.setText("Save preset");
        menuFileSavePreset.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuFileSavePresetActionPerformed(evt);
            }
        });
        menuFile.add(menuFileSavePreset);
        menuFile.add(jSeparator2);

        menuFileExit.setText("Exit");
        menuFileExit.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuFileExitActionPerformed(evt);
            }
        });
        menuFile.add(menuFileExit);

        menu.add(menuFile);

        menuMap.setText("Map");

        menuMapShow.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_M, java.awt.event.InputEvent.CTRL_MASK));
        menuMapShow.setText("Show");
        menuMapShow.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuMapShowActionPerformed(evt);
            }
        });
        menuMap.add(menuMapShow);

        menuMapOptions.setText("Options");
        menuMapOptions.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuMapOptionsActionPerformed(evt);
            }
        });
        menuMap.add(menuMapOptions);

        menu.add(menuMap);

        menuFG.setText("FlightGear");

        menuFGEnable.setText("Enable");
        menuFGEnable.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuFGEnableActionPerformed(evt);
            }
        });
        menuFG.add(menuFGEnable);

        menuFGOptions.setText("Options");
        menuFGOptions.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuFGOptionsActionPerformed(evt);
            }
        });
        menuFG.add(menuFGOptions);

        menu.add(menuFG);

        menuGCS.setText("GCSLink");

        menuGCSEnable.setText("Enable");
        menuGCSEnable.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuGCSEnableActionPerformed(evt);
            }
        });
        menuGCS.add(menuGCSEnable);

        menuGCSOptions.setText("Options");
        menuGCSOptions.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuGCSOptionsActionPerformed(evt);
            }
        });
        menuGCS.add(menuGCSOptions);

        menu.add(menuGCS);

        menuPlayback.setText("Playback");

        menuPlaybackPlay.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_X, java.awt.event.InputEvent.CTRL_MASK));
        menuPlaybackPlay.setText("Play");
        menuPlaybackPlay.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuPlaybackPlayActionPerformed(evt);
            }
        });
        menuPlayback.add(menuPlaybackPlay);

        menuPlaybackPause.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_C, java.awt.event.InputEvent.CTRL_MASK));
        menuPlaybackPause.setText("Pause");
        menuPlaybackPause.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuPlaybackPauseActionPerformed(evt);
            }
        });
        menuPlayback.add(menuPlaybackPause);

        menuPlaybackStop.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_V, java.awt.event.InputEvent.CTRL_MASK));
        menuPlaybackStop.setText("Stop");
        menuPlaybackStop.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                menuPlaybackStopActionPerformed(evt);
            }
        });
        menuPlayback.add(menuPlaybackStop);

        menu.add(menuPlayback);

        setJMenuBar(menu);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(panel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(panel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void menuFileExitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuFileExitActionPerformed
		dispose();
    }//GEN-LAST:event_menuFileExitActionPerformed

    private void menuFileOpenActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuFileOpenActionPerformed
		formatList.read();
		final List<FormatList.Info> formats = formatList.getAll();
		
		final String lastDir = appConfig.getOption(AppConfig.GENERAL, "lastdir");
		final JFileChooser fc = new JFileChooser(lastDir.isEmpty() ? getHomeDir() : lastDir);
		fc.setAcceptAllFileFilterUsed(false);
		
		for (FormatList.Info info : formats) 
			fc.addChoosableFileFilter(new FileNameExtensionFilter(info.name, info.ext));

		final String format = appConfig.getOption(AppConfig.GENERAL, "format");
		final FileFilter[] filters = fc.getChoosableFileFilters();
		for (FileFilter filter : filters)
			if (filter.getDescription().equals(format))
				fc.setFileFilter(filter);
		
		if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
			appConfig.setOption(AppConfig.GENERAL, "lastdir", fc.getSelectedFile().getParent());
			dataSource = new NullDataSource();
			final String formatName = fc.getFileFilter().getDescription();
			appConfig.setOption(AppConfig.GENERAL, "format", formatName);
			doLoadLog(fc.getSelectedFile().getPath(), formatName);
		}
    }//GEN-LAST:event_menuFileOpenActionPerformed

	private void panelMouseWheelMoved(java.awt.event.MouseWheelEvent evt) {//GEN-FIRST:event_panelMouseWheelMoved
		final GraphPane gp = getGraphPaneAt(evt.getX(), evt.getY());
		if (dataSource == null || gp == null)
			return;
		
		if (gp.isOnVerticalHeader(evt.getX())) {
			gp.changeYScale(evt.getWheelRotation(), evt.getY() - gp.getY());
		} else {
			double normX = (double) gp.getGraphX(evt.getX()) / gp.getGraphWidth();
			int pos = ofs + (int) (normX * getPointsVisible());
		
			scale -= evt.getWheelRotation();
			int newscale = scale;
			constrainOfsScale();
		
			if (newscale == scale) {
				ofs = pos - (int) (normX * getPointsVisible());
				constrainOfsScale();
			}

			for (Component c : panel.getComponents())
				if (c instanceof GraphPane)
					((GraphPane)c).setOfsScale(ofs, scale);
		}
	}//GEN-LAST:event_panelMouseWheelMoved

	private void panelMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_panelMousePressed
		if (evt.getButton() == MouseEvent.BUTTON1) {
			mouseButton[0] = true;
			mouseX = evt.getX();
			mouseY = evt.getY();
			if (playback == null || playback.getState() != Playback.PLAY)
				setCursorPosition(mouseX);
			lastgp = getGraphPaneAt(evt.getX(), evt.getY());
		} else if (evt.getButton() == MouseEvent.BUTTON2)
			mouseButton[1] = true;
		else if (evt.getButton() == MouseEvent.BUTTON3)
			mouseButton[2] = true;
		
		if (evt.isPopupTrigger())
			showPopupMenu(evt);
	}//GEN-LAST:event_panelMousePressed

	private void panelMouseDragged(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_panelMouseDragged
		if (!mouseButton[0] || dataSource == null || lastgp == null)
			return;

		if (lastgp.isOnVerticalHeader(evt.getX())) {
			lastgp.changeYOfs(mouseY - evt.getY());
			mouseY = evt.getY();
		} else {
			int mouseDelta = mouseX - evt.getX();
			mouseX = evt.getX();
		
			ofs += mouseDelta * getPointsVisible() / lastgp.getGraphWidth();
			constrainOfsScale();
		
			for (Component c : panel.getComponents())
				if (c instanceof GraphPane)
					((GraphPane)c).setOfsScale(ofs, scale);
		}
	}//GEN-LAST:event_panelMouseDragged

	private void panelMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_panelMouseReleased
		if (evt.getButton() == MouseEvent.BUTTON1)
			mouseButton[0] = false;
		else if (evt.getButton() == MouseEvent.BUTTON2)
			mouseButton[1] = false;
		else if (evt.getButton() == MouseEvent.BUTTON3)
			mouseButton[2] = false;

		if (evt.isPopupTrigger())
			showPopupMenu(evt);
	}//GEN-LAST:event_panelMouseReleased

	private void panelMouseMoved(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_panelMouseMoved
		if (playback == null || playback.getState() == Playback.IDLE)
			setCursorPosition(evt.getX());
	}//GEN-LAST:event_panelMouseMoved

	private void menuFileOpenPresetActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuFileOpenPresetActionPerformed
		final JFileChooser fc = createPresetDialog();
		if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
			graphPaneConfig.open(fc.getSelectedFile().getPath(), dataSource);
			syncToGraphPaneConfig();
		}
	}//GEN-LAST:event_menuFileOpenPresetActionPerformed

	private void menuFileSavePresetActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuFileSavePresetActionPerformed
		final JFileChooser fc = createPresetDialog();
		if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
			String path = fc.getSelectedFile().getPath();
			if (!path.endsWith(".xml"))
				path += ".xml";
			graphPaneConfig.save(path);
		}
	}//GEN-LAST:event_menuFileSavePresetActionPerformed

	private JFileChooser createPresetDialog() {
		final JFileChooser fc = new JFileChooser(getHomeDir() + "/data/presets");
		fc.setFileFilter(new FileNameExtensionFilter("ZorLog preset", "xml"));
		fc.setAcceptAllFileFilterUsed(false);
		return fc;
	}
	
	private void menuMapShowActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuMapShowActionPerformed
		if (wwf != null && wwf.isVisible())
			return;
		
		wwf = new WorldWindFrame(getHomeDir(), wwfBounds, new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				wwfBounds = wwf.getBounds();
			}
		});
		
		wwf.setVisible(true);
		applyMapOptions();
		wwf.setTrack(dataSource, true);
	}//GEN-LAST:event_menuMapShowActionPerformed

	private void menuPlaybackPlayActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuPlaybackPlayActionPerformed
		if (playback == null)
			return;
		
		if (playbackTimer != null)
			playbackTimer.stop();
		
		playback.play(cursor);
		
		playbackTimer = new Timer(50, new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (playback == null || playback.getState() == Playback.IDLE)
					playbackTimer.stop();
				else if (playback.getState() == Playback.PLAY) {
					cursor = Math.max(0, Math.min(playback.getIndex(), dataSource.getSize() - 1));
					updateCursorPosition();
				}
			}
		});
		
		playbackTimer.start();
	}//GEN-LAST:event_menuPlaybackPlayActionPerformed

	private void menuPlaybackPauseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuPlaybackPauseActionPerformed
		if (playback == null)
			return;
		
		if (playback.getState() == Playback.PLAY)
			playback.pause();
		else if (playback.getState() == Playback.PAUSE)
			playback.play(cursor);
	}//GEN-LAST:event_menuPlaybackPauseActionPerformed

	private void menuPlaybackStopActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuPlaybackStopActionPerformed
		if (playback == null)
			return;
		
		playback.stop();
	}//GEN-LAST:event_menuPlaybackStopActionPerformed

	private void menuFileSaveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuFileSaveActionPerformed
		final String lastDir = appConfig.getOption(AppConfig.GENERAL, "lastdir");
		final JFileChooser fc = new JFileChooser((lastDir != null) ? lastDir : getHomeDir());
		
		fc.addChoosableFileFilter(new FileNameExtensionFilter("Text log", "txt"));
		fc.addChoosableFileFilter(new FileNameExtensionFilter("Google Earth KML", "kml"));		
		final FileFilter[] filters = fc.getChoosableFileFilters();
		for (FileFilter filter : filters)
			if (filter.getDescription().equals("Text log"))
				fc.setFileFilter(filter);

		if (logfilename != null)
			fc.setSelectedFile(new File(logfilename));

		if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
			String path = fc.getSelectedFile().getPath();
			appConfig.setOption(AppConfig.GENERAL, "lastdir", fc.getSelectedFile().getParent());

			final String formatName = fc.getFileFilter().getDescription();
			String ext = ".txt";
			if ("Google Earth KML".equalsIgnoreCase(formatName))
				ext = ".kml";
			
			if (!path.endsWith(ext))
				path += ext;
			doSaveLog(path, formatName);
		}
	}//GEN-LAST:event_menuFileSaveActionPerformed

	private void menuFGOptionsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuFGOptionsActionPerformed
		final FlightGearOptions fgOptions = new FlightGearOptions(dataSource.getNames());
		fgOptions.readValues(appConfig);
		
		final int res = JOptionPane.showConfirmDialog(
				this, fgOptions, 
				"FlightGear Options", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
		
		if (res == JOptionPane.OK_OPTION) {
			fgOptions.writeValues(appConfig);
			appConfig.write();
			applyAppOptions();
		}
	}//GEN-LAST:event_menuFGOptionsActionPerformed

	private void menuMapOptionsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuMapOptionsActionPerformed
		final MapOptions mapOptions = new MapOptions(dataSource.getNames());
		mapOptions.readValues(appConfig);
		
		final int res = JOptionPane.showConfirmDialog(
				this, mapOptions, 
				"Map Options", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
		
		if (res == JOptionPane.OK_OPTION) {
			mapOptions.writeValues(appConfig);
			appConfig.write();
			applyAppOptions();			
		}
	}//GEN-LAST:event_menuMapOptionsActionPerformed

	private void menuFGEnableActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuFGEnableActionPerformed
		fgSender = menuFGEnable.getState() ? createFlightGearSender() : null;
	}//GEN-LAST:event_menuFGEnableActionPerformed

	private void formWindowClosed(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosed
		savePosSize();
		if (wwf != null && wwf.isVisible())
			wwf.dispose();
		System.exit(0);
	}//GEN-LAST:event_formWindowClosed

	private void menuGCSEnableActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuGCSEnableActionPerformed
		if (menuGCSEnable.getState())
			createGCSLink();
		else
			killGCSLink();
	}//GEN-LAST:event_menuGCSEnableActionPerformed

	private void menuGCSOptionsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_menuGCSOptionsActionPerformed
		final GCSOptions gcsOptions = new GCSOptions(dataSource.getNames());
		gcsOptions.readValues(appConfig);
		
		final int res = JOptionPane.showConfirmDialog(
				this, gcsOptions, 
				"Map Options", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
		
		if (res == JOptionPane.OK_OPTION) {
			gcsOptions.writeValues(appConfig);
			appConfig.write();
			applyAppOptions();			
		}
	}//GEN-LAST:event_menuGCSOptionsActionPerformed
	

	private FlightGearSender createFlightGearSender() {
		final String gr = AppConfig.FLIGHTGEAR;
		
		double filter = 1;
		if ("1".equals(appConfig.getOption(gr, "filterenable")))
			filter = Double.parseDouble(appConfig.getOption(gr, "filteralpha"));

		double offset = 0;
		if ("1".equals(appConfig.getOption(gr, "offsetenable")))
			offset = Double.parseDouble(appConfig.getOption(gr, "offset"));
		
		return new FlightGearSender(
				appConfig.getOption(gr, "dst"),
				Integer.parseInt(appConfig.getOption(gr, "dstport")),
				appConfig.getOption(gr, "self"),
				appConfig.getOption(gr, "latitude"),
				appConfig.getOption(gr, "longitude"),
				appConfig.getOption(gr, "altitude"),
				appConfig.getOption(gr, "airspeed"),
				appConfig.getOption(gr, "roll"),
				appConfig.getOption(gr, "pitch"),
				appConfig.getOption(gr, "heading"),
				appConfig.getOption(gr, "aileron"),
				appConfig.getOption(gr, "elevator"),
				appConfig.getOption(gr, "rudder"),
				appConfig.getOption(gr, "throttle"),
				appConfig.getOption(gr, "flaps"),
				appConfig.getOption(gr, "rpm"),
				appConfig.getOption(gr, "gspeed"),
				appConfig.getOption(gr, "baroalt"),
				appConfig.getOption(gr, "vspeed"),
				appConfig.getOption(gr, "aoa"),
				appConfig.getOption(gr, "slip"),
				filter, offset);
	}
	
	private void createGCSLink() {
		final String gr = AppConfig.GCS;
		
		SerialStream port = "serial".equals(appConfig.getOption(gr, "link")) ? 
			new COMStream(
					appConfig.getOption(gr, "comport"),
					Integer.parseInt(appConfig.getOption(gr, "baudrate"))) :
			new UDPStream(
					appConfig.getOption(gr, "self"),
					Integer.parseInt(appConfig.getOption(gr, "selfport")),
					appConfig.getOption(gr, "remote"),
					Integer.parseInt(appConfig.getOption(gr, "remoteport")));
		
		gcsLink = new GCSLink(
				port,
				appConfig.getOption(gr, "state"),
				appConfig.getOption(gr, "roll"),
				appConfig.getOption(gr, "pitch"),
				appConfig.getOption(gr, "heading"),
				appConfig.getOption(gr, "airspeed"),
				appConfig.getOption(gr, "baroalt"),
				appConfig.getOption(gr, "vspeed"),
				appConfig.getOption(gr, "course"),
				appConfig.getOption(gr, "gspeed"),
				appConfig.getOption(gr, "latitude"),
				appConfig.getOption(gr, "longitude"),
				appConfig.getOption(gr, "altitude"),
				appConfig.getOption(gr, "rpm"),
				appConfig.getOption(gr, "temp1"),
				appConfig.getOption(gr, "temp2"),
				appConfig.getOption(gr, "aileron"),
				appConfig.getOption(gr, "elevator"),
				appConfig.getOption(gr, "rudder"),
				appConfig.getOption(gr, "throttle"),
				appConfig.getOption(gr, "flaps"),
				appConfig.getOption(gr, "failures"),
				appConfig.getOption(gr, "voltage"),
				appConfig.getOption(gr, "current"),
				appConfig.getOption(gr, "fuel"));
		
		gcsLinkThread = new Thread(gcsLink);
		gcsLinkThread.start();
	}

	private void killGCSLink() {
		if (gcsLink == null)
			return;
		
		gcsLink.close();
		try {
			gcsLinkThread.join();	// wait thread to die
		} catch (InterruptedException ex) {
			Logger.getLogger(ZorLogFrame.class.getName()).log(Level.SEVERE, null, ex);
		}
		gcsLink = null;
		gcsLinkThread = null;
	}
	
	private void applyAppOptions() {
		if (fgSender != null)
			fgSender = createFlightGearSender();
		
		applyMapOptions();
		if (wwf != null)
			wwf.setTrack(dataSource, false);
		
		if (gcsLink != null) {
			killGCSLink();
			createGCSLink();
		}
		
		updateCursorPosition();
	}
	
	private void applyMapOptions() {
		if (wwf == null)
			return;

		final String gr = AppConfig.MAP;

		double offset = 0;
		if ("1".equals(appConfig.getOption(gr, "offsetenable")))
			offset = Double.parseDouble(appConfig.getOption(gr, "offset"));
			
		wwf.setOptions(
				appConfig.getOption(gr, "latitude"),
				appConfig.getOption(gr, "longitude"),
				appConfig.getOption(gr, "altitude"),
				offset,
				appConfig.getOption(gr, "roll"),
				appConfig.getOption(gr, "pitch"),
				appConfig.getOption(gr, "heading"));
	}
	
	
	private GraphPane getGraphPaneAt(int x, int y) {
		final Component underCursor = panel.getComponentAt(x, y);
		if (underCursor == null || !(underCursor instanceof GraphPane))
			return null;
		
		return (GraphPane)underCursor;
	}
	
	
	/** 
	 * Loads log using LogLoaderWorker.
	 * LogLoaderWorker extends SwingWorker, so the sequence is:
	 * 1. Initialize progressbar and disable UI
	 * 2. Create LogLoaderWorker passing log file name and logformat file name into costructor
	 * 3. Add PropertyChangeListener, it will update progressbar by property "progress" and 
	 *    it will update dataSource ref and enable UI by property "state"
	 * 4. Start the process in separate thread by method execute()
	 */
	private void doLoadLog(final String log, String formatName) {
		final String xmlFileName = formatList.getByName(formatName).fileName;
		final String type = formatList.getByName(formatName).type;
		
		if (playback != null)
			playback.stop();
		
		progressBar.setValue(0);
		getGlassPane().setVisible(true);
		
		final LogLoaderWorker loader = new LogLoaderWorker(log, xmlFileName, type);

		loader.addPropertyChangeListener(
				new PropertyChangeListener() {
					@Override
					public void propertyChange(PropertyChangeEvent evt) {
						if ("progress".equals(evt.getPropertyName())) {
							progressBar.setValue((Integer)evt.getNewValue());
						} else if ("state".equals(evt.getPropertyName())
								&& (loader.getState() == SwingWorker.StateValue.DONE)) {
							try {
								dataSource = loader.get();
							} catch (InterruptedException | ExecutionException ex) {
								Logger.getLogger(ZorLogFrame.class.getName()).log(Level.SEVERE, null, ex);
							}
							
							onLoadLog(log);
						}
					}
				});

		loader.execute();
	}

	
	/**
	 * Called after loading log.
	 */
	private void onLoadLog(final String log) {
		getGlassPane().setVisible(false);
		
		if (graphPaneConfig.getPaneList().isEmpty()) {
			graphPaneConfig.open(getHomeDir() + "/data/presets/default.xml", dataSource);
			syncToGraphPaneConfig();
		}
		
		scale = 0;
		ofs = 0;
		cursor = 0;
		updateGraphPanes();
		updateTitle(log);
		
		logfilename = log;
		
		if (wwf != null)
			wwf.setTrack(dataSource, true);
		
		playback = new Playback(dataSource.getTimeSource().getPlaybackRate());
	}
	
	/** 
	 * Saves log using LogSaverWorker (see doLoadLog)
	 */
	private void doSaveLog(final String log, final String formatName) {
		if (playback != null)
			playback.stop();
		
		progressBar.setValue(0);
		getGlassPane().setVisible(true);
		
		final LogSaverWorker saver = new LogSaverWorker(log, formatName, dataSource);

		saver.addPropertyChangeListener(
				new PropertyChangeListener() {
					@Override
					public void propertyChange(PropertyChangeEvent evt) {
						if ("progress".equals(evt.getPropertyName())) {
							progressBar.setValue((Integer)evt.getNewValue());
						} else if ("state".equals(evt.getPropertyName())
								&& (saver.getState() == SwingWorker.StateValue.DONE)) {
							getGlassPane().setVisible(false);
						}
					}
				});

		saver.execute();
	}
		
	
	private void showPopupMenu(java.awt.event.MouseEvent evt) {
		final JPopupMenu m = new JPopupMenu();
		JMenuItem item = new JMenuItem("Add pane");
		item.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				popupAddPaneActionPerformed(e);
			}
		});
		m.add(item);
		
		lastgp = getGraphPaneAt(evt.getX(), evt.getY());
		
		if (lastgp != null) {
			item = new JMenuItem("Delete pane");
			item.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					popupDeletePaneActionPerformed(e);
				}
			});
			m.add(item);
			
			item = new JMenuItem("Edit graphs");
			item.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					popupEditGraphsActionPerformed(e);
				}
			});
			m.add(item);
		}
		
		m.show(this,
			evt.getLocationOnScreen().x - getLocationOnScreen().x, 
			evt.getLocationOnScreen().y - getLocationOnScreen().y);
	}
	
	private void popupAddPaneActionPerformed(java.awt.event.ActionEvent evt) {
		addGraphPane();
		updateCursorPosition();
	}

	private void popupDeletePaneActionPerformed(java.awt.event.ActionEvent evt) {
		if (lastgp != null)
			deleteGraphPane(lastgp);
	}

	private void popupEditGraphsActionPerformed(java.awt.event.ActionEvent evt) {
		if (lastgp == null)
			return;
		
		final GraphSelector graphSelector = new GraphSelector();
		graphSelector.setAvailable(dataSource.getNames());
		graphSelector.setSelected(lastgp.getGraphNames());
		
		final int res = JOptionPane.showConfirmDialog(
				this, graphSelector, 
				"Select graph", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
		
		if (res == JOptionPane.OK_OPTION) {
			lastgp.removeAllGraph();
			for (String name : graphSelector.getSelected())
				lastgp.addGraph(dataSource, name);
		}
	}
	

	/**
	 * Adds graph pane to panel
	 */
	private void addGraphPane() {
		GraphPane gp = graphPaneConfig.addPane();
		gp.setOfsScale(ofs, scale);
		gp.setCursorPosition(cursor);
		panel.add(gp);
		panel.revalidate();
		panel.repaint();
	}
	
	private void deleteGraphPane(GraphPane gp) {
		graphPaneConfig.deletePane(gp);
		panel.remove(gp);
		panel.revalidate();
		panel.repaint();
	}
	
	
	/**
	 * Deletes all graph panes from panel
	 */
	private void syncToGraphPaneConfig() {
		for (Component c : panel.getComponents())
			if (c instanceof GraphPane)
				panel.remove(c);

		for (GraphPane gp : graphPaneConfig.getPaneList())
			panel.add(gp);
		
		panel.revalidate();
		panel.repaint();
	}
	
	/**
	 * Returns first graph pane in panel
	 */
	private GraphPane getFirstGraphPane() {
		for (Component c : panel.getComponents())
			if (c instanceof GraphPane)
				return (GraphPane)c;
		return null;
	}
	
	/**
	 * Limits values of ofs and scale
	 */
	private void constrainOfsScale() {
		final GraphPane gp = getFirstGraphPane();
		if (gp == null)
			return;

		if (scale < 0)
			scale = 0;
		
		while (getPointsVisible() < gp.getGraphWidth() / 16 && scale > 0)
			--scale;

		if (ofs < 0)
			ofs = 0;
		else if (ofs + getPointsVisible() > dataSource.getSize())
			ofs = dataSource.getSize() - getPointsVisible();
	}
	
	
	/**
	 * Sets position of cursor
	 */
	private void setCursorPosition(int x) {
		final GraphPane gp = getFirstGraphPane();
		if (gp == null)
			return;
		
		double normX = (double) gp.getGraphX(x) / gp.getGraphWidth();
		cursor = ofs + (int) (normX * getPointsVisible());
		cursor = Math.min(Math.max(cursor, 0), dataSource.getSize() - 1);
		updateCursorPosition();
	}
	

	private void updateCursorPosition() {
		for (Component c : panel.getComponents())
			if (c instanceof GraphPane)
				((GraphPane)c).setCursorPosition(cursor);

		if (wwf != null)
			wwf.setCursor(dataSource, cursor);

		if (fgSender != null)
			fgSender.send(dataSource, cursor);
		
		if (gcsLink != null)
			gcsLink.setData(dataSource, cursor);
	}
	
	
	/**
	 * @return Количество видимых точек при заданном масштабе 
	 */
	private int getPointsVisible() {
		return dataSource.getSize() / (1 << scale);
	}
	
	/**
	 * update graphs
	 */
	private void updateGraphPanes() {
		for (Component c : panel.getComponents()) {
			if (c instanceof GraphPane) {
				GraphPane gp = (GraphPane)c;
				gp.setOfsScale(ofs, scale);				

				List<Graph> graphs = gp.getGraphs();
				gp.removeAllGraph();
				for (Graph graph : graphs)
					gp.addGraph(dataSource, graph.getName(), graph.getColor());
			}
		}
	}

	/**
	 * Creates glass pane containing progress bar.
	 * To make it visible call getGlassPane().setVisible(true), progress bar available as progressBar
	 */
	private void initGlassPane() {
		final JPanel gp = new AlphaPane(0.3f);
		gp.setOpaque(false);
		gp.setLayout(new BoxLayout(gp, BoxLayout.Y_AXIS));
		
		progressBar = new JProgressBar();
		progressBar.setMaximumSize(new Dimension(400, 20));
        progressBar.setStringPainted(true);
		
		gp.add(new Box.Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)));
		gp.add(progressBar);
		gp.add(new Box.Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)));
		
		setGlassPane(gp);
	}
	
	
	/**
	 * Returns path to home directory
	 */
	private String getHomeDir() {
		return System.getProperty("user.dir");
	}

	/**
	 * Updates frame title.
	 * @param filename loaded log filename
	 */
	private void updateTitle(String filename) {
		setTitle(APPTITLE + (filename != null ? " [" +  filename + "]" : ""));
	}

	
	private void applyPosSize() {
		setBounds(
				Integer.parseInt(appConfig.getOption(AppConfig.MAIN_WINDOW, "x")),
				Integer.parseInt(appConfig.getOption(AppConfig.MAIN_WINDOW, "y")), 
				Integer.parseInt(appConfig.getOption(AppConfig.MAIN_WINDOW, "w")), 
				Integer.parseInt(appConfig.getOption(AppConfig.MAIN_WINDOW, "h")));
		
		wwfBounds = new Rectangle(
				Integer.parseInt(appConfig.getOption(AppConfig.MAP_WINDOW, "x")),
				Integer.parseInt(appConfig.getOption(AppConfig.MAP_WINDOW, "y")), 
				Integer.parseInt(appConfig.getOption(AppConfig.MAP_WINDOW, "w")), 
				Integer.parseInt(appConfig.getOption(AppConfig.MAP_WINDOW, "h")));
	}
	
	private void savePosSize() {
		final Rectangle r = getBounds();
		appConfig.setOption(AppConfig.MAIN_WINDOW, "x", Integer.toString(r.x));
		appConfig.setOption(AppConfig.MAIN_WINDOW, "y", Integer.toString(r.y));
		appConfig.setOption(AppConfig.MAIN_WINDOW, "w", Integer.toString(r.width));
		appConfig.setOption(AppConfig.MAIN_WINDOW, "h", Integer.toString(r.height));
		
		appConfig.setOption(AppConfig.MAP_WINDOW, "x", Integer.toString(wwfBounds.x));
		appConfig.setOption(AppConfig.MAP_WINDOW, "y", Integer.toString(wwfBounds.y));
		appConfig.setOption(AppConfig.MAP_WINDOW, "w", Integer.toString(wwfBounds.width));
		appConfig.setOption(AppConfig.MAP_WINDOW, "h", Integer.toString(wwfBounds.height));
		appConfig.write();
	}
	
	
	
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JMenu menuFG;
    private javax.swing.JCheckBoxMenuItem menuFGEnable;
    private javax.swing.JMenuItem menuFGOptions;
    private javax.swing.JMenuItem menuFileSave;
    private javax.swing.JMenu menuGCS;
    private javax.swing.JCheckBoxMenuItem menuGCSEnable;
    private javax.swing.JMenuItem menuGCSOptions;
    private javax.swing.JMenuItem menuMapOptions;
    private javax.swing.JPanel panel;
    // End of variables declaration//GEN-END:variables


	private AppConfig appConfig;
	private FormatList formatList;
	
	private JProgressBar progressBar;
	private DataSource dataSource = new NullDataSource();
	private int scale = 0;
	private int ofs = 0;
	private int cursor = 0;
	private int mouseX = 0;
	private int mouseY = 0;
	private boolean[] mouseButton = new boolean[3];
	private GraphPane lastgp;
	private GraphPaneConfig graphPaneConfig = new GraphPaneConfig();
	
	private String logfilename;
	
	private WorldWindFrame wwf;
	private Rectangle wwfBounds;
	
	private Playback playback;
	private Timer playbackTimer;
	
	private FlightGearSender fgSender;
	private GCSLink gcsLink;
	private Thread gcsLinkThread;
}
